package saves;

import com.esotericsoftware.kryo.Kryo;
import static game.Configurations.*;
import game.Data;
import game.MapConstraints;
import game.Units;
import images.BlockLoader;
import images.ImageInfo;
import images.ObjectLoader;
import java.awt.Color;
import java.io.*;
import java.util.ArrayList;
import math.Point;
import org.jbox2d.callbacks.ContactImpulse;
import org.jbox2d.callbacks.ContactListener;
import org.jbox2d.collision.Manifold;
import org.jbox2d.collision.shapes.CircleShape;
import org.jbox2d.collision.shapes.PolygonShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.BodyType;
import org.jbox2d.dynamics.FixtureDef;
import org.jbox2d.dynamics.World;
import org.jbox2d.dynamics.contacts.Contact;
import saves.objects.Construcao;
import saves.objects.Corpo;
import saves.objects.Explosao;
import saves.objects.Nave;
import saves.objects.SeletorBloco;

public class Area implements Serializable {

    private static final long serialVersionUID = -8825858157510050933L;
    private BlocoSimples[][] blocos;
    private ArrayList<Corpo> objetos;
    private ArrayList<Explosao> explosoes;
    private Passagem[] passagens;
    private Cidade[] cidades;
    private Point deltaS;
    private int largura, altura; //da tela, para realizar o desenho
    private int referencia; //padrão do objeto observador
    //selection data
    private transient ArrayList<Corpo> selectables;
    private transient Corpo selecao; //objeto selecionado pelo player
    private transient boolean locked; //todo add target locking
    
    private World world;
    
    public Area() { }
    
    public Area(Area a) { //creates an 'empty' copy, to be filled with known info
        blocos = new BlocoSimples[a.blocos.length][a.blocos[0].length]; //assuming it's a square matrix
        objetos = new ArrayList<>();
        explosoes = new ArrayList<>();
        passagens = new Passagem[0];
        cidades = new Cidade[0];
        //the rest of the attributes is not used
        selecao = null;
        locked = false;
        setUpWorld();
    }

    public Area(int[][] blocos) {
        this(blocos, SCREEN_WIDTH, GAME_HEIGHT);
    }

    public Area(int[][] blocos, int largura, int altura) {
        this(blocos, new ArrayList<Corpo>(), new Passagem[0], new Cidade[0], largura, altura);
    }

    public Area(int[][] blocos, ArrayList<Corpo> objetos, Passagem[] passagens, Cidade[] cidades, int largura, int altura) {
        this(Area.converterMatriz(blocos), objetos, passagens, cidades, largura, altura);
    }

    public Area(BlocoSimples[][] blocos) {
        this(blocos, SCREEN_WIDTH, GAME_HEIGHT);
    }

    public Area(BlocoSimples[][] blocos, int largura, int altura) {
        this(blocos, new ArrayList<Corpo>(), new Passagem[0], new Cidade[0], largura, altura);
    }

    public Area(BlocoSimples[][] blocos, ArrayList<Corpo> objetos, Passagem[] passagens, Cidade[] cidades, int largura, int altura) {
        this.blocos = blocos;
        this.objetos = objetos;
        this.explosoes = new ArrayList<>();
        this.passagens = passagens;
        this.cidades = cidades;
        this.deltaS = new Point();
        this.largura = largura;
        this.altura = altura;
        this.referencia = -1;
        this.selecao = null;
        setUpWorld();
    }

    public World getWorld() {
        return world;
    }
    
    private void setUpWorld() {
        world = new World(new Vec2()); //no gravity ((0, 0))
        world.setAutoClearForces(false);
        world.setContactListener(new ContactListener() {

            @Override
            public void beginContact(Contact contact) { }

            @Override
            public void endContact(Contact contact) { }

            @Override
            public void preSolve(Contact contact, Manifold oldManifold) { }

            @Override
            public void postSolve(Contact contact, ContactImpulse impulse) {
                Body ba = contact.getFixtureA().getBody(),
                     bb = contact.getFixtureB().getBody();
                
                if (ba.getUserData() instanceof Explosao) {
                    float power = ((Explosao) ba.getUserData()).getPower();
                    if (bb.getUserData() != null)
                        ((Corpo) bb.getUserData()).harm(ba.getLinearVelocity().length() * power/2, EntidadeNave.DamageType.EXPLOSION, ((Explosao) ba.getUserData()).getOwner());
                } else if (bb.getUserData() instanceof Explosao) { 
                    float power = ((Explosao) bb.getUserData()).getPower();
                    if (ba.getUserData() != null)
                        ((Corpo) ba.getUserData()).harm(bb.getLinearVelocity().length() * power/2, EntidadeNave.DamageType.EXPLOSION, ((Explosao) bb.getUserData()).getOwner());
                } else {
                    //regular collision
                    if (((Corpo) ba.getUserData()) != null)
                        ((Corpo) ba.getUserData()).collided((Corpo) bb.getUserData());
                    if ((Corpo) bb.getUserData() != null)
                        ((Corpo) bb.getUserData()).collided((Corpo) ba.getUserData());
                }
            }
        });
        addBlocks();
    }
    
    //must be called to update collision info everytime a block changes
    private void addBlocks() {
        //remove all static objects prior in the world
        for (Body b = world.getBodyList(); b != null; b = b.getNext())
            if (b.getType() == BodyType.STATIC) {
                world.destroyBody(b);
                b = world.getBodyList();
            }
        for (int i = 0; i < blocos.length; i++)
            for (int j = 0; j < blocos[i].length; j++)
                if (blocos[i][j] != null && Data.getBlocoInfo(blocos[i][j].getCodigo()).getResistenciaAr() == 1) {
                    BodyDef bd = new BodyDef();
                    bd.type = BodyType.STATIC;
                    bd.position = new Vec2(Units.getRealSize(i*BLOCK_SIZE + BLOCK_SIZE/2), Units.getRealSize(j*BLOCK_SIZE + BLOCK_SIZE/2));

                    PolygonShape ps = new PolygonShape();
                    ps.setAsBox(Units.getRealSize(BLOCK_SIZE/2), Units.getRealSize(BLOCK_SIZE/2));

                    FixtureDef fd = new FixtureDef();
                    fd.shape = ps;
                    fd.friction = 0;
                    fd.restitution = .1f;

                    world.createBody(bd).createFixture(fd);
                }
    }
    
    public void updateSubBlocos() {
        for (int i = 0; i < blocos.length; i++)
            for (int j = 0; j < blocos[i].length; j++)
                setSubBlocoDe(i, j);
    }
    
    public int getMaxX() {
        return BLOCK_SIZE * blocos.length;
    }
    
    public int getMaxY() {
        return BLOCK_SIZE * blocos[0].length;
    }
    
    public void addImplosion(Vec2 center, float attraction, EntidadeNave owner) {
        explosoes.add(new Explosao(center.x, center.y, attraction, 1500, owner));
    }

    public void addExplosion(Vec2 center, float blastPower, EntidadeNave owner) {
        final int numRays = 25;
        final long time = 750;
        Explosao e = new Explosao(center.x, center.y, blastPower, time, owner);
        Body[] bs = new Body[numRays];
        for (int i = 0; i < numRays; i++) {
            float angle = (float) Math.toRadians((i / (float) numRays) * 360);
            Vec2 rayDir = new Vec2((float) Math.sin(angle), (float) Math.cos(angle));

            BodyDef bd = new BodyDef();
            bd.type = BodyType.DYNAMIC;
            bd.fixedRotation = true; // rotation not necessary
            bd.bullet = true; // prevent tunneling at high speed
            bd.linearDamping = 0; // drag due to moving through air
            bd.position = center; // start at blast center
            float rand = (float) Math.random() - 0.5f;
            bd.linearVelocity = rayDir.mul(blastPower + blastPower*rand/2);
            bd.angularVelocity = rand*5;
            Body body = world.createBody(bd);

            CircleShape circleShape = new CircleShape();
            circleShape.m_radius = 0.05f; // very small

            FixtureDef fd = new FixtureDef();
            fd.shape = circleShape;
            fd.density = 20000 / (float)numRays; // very high - shared across all particles
            fd.friction = 0; // friction not necessary
            fd.restitution = 0.99f; // high restitution to reflect off obstacles
            fd.filter.groupIndex = -1; // particles should not collide with each other
            body.createFixture(fd);
            body.setUserData(e);
            
            bs[i] = body;
        }
        e.setParticles(bs);
        explosoes.add(e);
    }
    
    public static BlocoSimples[][] converterMatriz(int[][] blocos) {
        BlocoSimples[][] m = new BlocoSimples[blocos.length][blocos[0].length];
        for (int i = 0; i < m.length; i++)
            for (int j = 0; j < m[i].length; j++)
                m[i][j] = BlocoSimples.getBloco(blocos[i][j]);
        return m;
    }

    private static String getPathFor(String nome, int save, int[] areaAtual) {
        String codArea = String.valueOf(areaAtual[0]);
        for (int i = 1; i < areaAtual.length; i++)
            codArea += "." + i;
        return SaveManager.SAVE_PATH + nome + "\\saves\\" + save + "\\areas\\" + codArea + ".area";
    }

    public static Area carregarArea(String nome, int save, int[] areaAtual) {
        return carregarArea(getPathFor(nome, save, areaAtual));
    }

    public static Area carregarArea(String caminho) {
        Area a = null;
        try (ObjectInputStream leitor = new ObjectInputStream(new FileInputStream(caminho))) {
            a = (Area) leitor.readObject();
        } catch (ClassNotFoundException | IOException e) {
        }
        return a;
    }

    public static boolean salvarArea(String nome, int save, int[] areaAtual, Area area) {
        return salvarArea(getPathFor(nome, save, areaAtual), area);
    }

    public static boolean salvarArea(String caminho, Area area) {
        try (ObjectOutputStream escritor = new ObjectOutputStream(new FileOutputStream(caminho))) {
            escritor.writeObject(area);
        } catch (IOException e) {
            return false;
        }
        return true;
    }
    
    public void updateShapes() {
        for (int i = 0; i < objetos.size(); i++)
            if (objetos.get(i) instanceof Nave)
                ((Nave) objetos.get(i)).setupShape();
    }

    private int lastBit;
    public void update(int delta) {
        for (int i = 0; i < explosoes.size(); i++)
            if (explosoes.get(i).update(delta, this))
                explosoes.remove(i--).destroyParticles(world);
        
        final float dt_step = 15;
        final int step_max = 20;
        int t;
        
        delta += lastBit;
        lastBit = 0;
        for (t = 0; t <= step_max*dt_step && t < delta; t += dt_step)
            world.step(dt_step, 6, 3); //constant for precision
        if (t > step_max*dt_step)
            System.err.println("Can't keep up! Lag spike!");
        else if (delta  > t)
            //world.step(delta - t, 6, 3); //constant for precision
            lastBit = delta - t;
        world.clearForces();
        
        for (int i = 0; i < objetos.size(); i++)
            if (objetos.get(i).paraDestruir()) {
                if (referencia == i) {
                    //todo death
                    System.err.println("Você morreu!");
                    System.exit(0);
                } else if (selecao == objetos.get(i)) {
                    selecao = null;
                    locked = false;
                }
                world.destroyBody(objetos.remove(i--).getBody());
            } else
                objetos.get(i).update(delta, this); //updates things unrelated to the physics engine
        selectNearest();
    }

    public void setAc(int ac) {
        if (this.objetos.get(this.referencia) instanceof Nave)
            ((Nave) this.objetos.get(this.referencia)).setAcc(ac);
    }

    public void girar(int cw) {
        if (this.objetos.get(this.referencia) instanceof Nave)
            ((Nave) this.objetos.get(this.referencia)).setRotation(cw);
    }
    
    public Corpo getReferencia() {
        return this.getObjeto(referencia);
    }

    public int setReferencia() {
        int oldRef = this.referencia;
        this.referencia = objetos.size() - 1;
        return oldRef;
    }

    public boolean contem(Point.Inteiro p) {
        return contem(p.x, p.y);
    }

    public boolean contem(float x, float y) {
        return (x >= 0 && x <= BLOCK_SIZE * (blocos.length - 1) && y >= 0 && y <= BLOCK_SIZE * (blocos[0].length - 1));
    }

    public boolean contemBloco(int x, int y) {
        return (x >= 0 && x < blocos.length && y >= 0 && y < blocos[0].length);
    }

    public int getBloco(int x, int y) {
        return blocos[x][y].getCodigo();
    }

    public BlocoSimples getBlocoSimples(int x, int y) {
        return blocos[x][y];
    }

    public boolean podeTerCircuito(int x, int y) {
        return Data.getBlocoInfo(blocos[x][y].getCodigo()).getFiacao() > 0;
    }

    public void setBloco(int x, int y, int bloco) {
        blocos[x][y] = BlocoSimples.getBloco(bloco);
        atualizarSubBlocos(x, y);
    }

    public Corpo getObjeto(int pos) {
        return objetos.get(pos);
    }

    public void addObjeto(Corpo objeto) {
        this.objetos.add(objeto);
    }

    public Corpo getPrimeiroObjeto() {
        return getObjeto(0);
    }

    public Corpo getUltimoObjeto() {
        return getObjeto(objetos.size() - 1);
    }

    public void removerSeletores(int newRef) {
        this.referencia = newRef;
        for (int i = 0; i < objetos.size(); i++)
            if (objetos.get(i) instanceof SeletorBloco) {
                objetos.remove(i);
                if (this.referencia > i)
                    this.referencia--;
                i--;
            }
    }

    public boolean isLocked() {
        return locked;
    }

    public void setLocked(boolean locked) {
        this.locked = locked;
    }

    public ArrayList<Corpo> getObjetos() {
        return objetos;
    }

    public void setObjeto(int pos, Corpo obj) {
        this.objetos.set(pos, obj);
    }

    public void removeObjeto(int pos) {
        this.objetos.remove(pos);
    }

    public int qtosObjetos() {
        return this.objetos.size();
    }

    public float desconverterX(int x) {
        return x + deltaS.x;
    }

    public float desconverterY(int y) {
        return y + deltaS.y;
    }
    
    public void atualizarSubBlocos(int x, int y) {
        if (contemBloco(x, y)) {
            if (contemBloco(x - 1, y))
                setSubBlocoDe(x - 1, y);
            if (contemBloco(x + 1, y))
                setSubBlocoDe(x + 1, y);
            if (contemBloco(x, y - 1))
                setSubBlocoDe(x, y - 1);
            if (contemBloco(x, y + 1))
                setSubBlocoDe(x, y + 1);
            if (contemBloco(x - 1, y - 1))
                setSubBlocoDe(x - 1, y - 1);
            if (contemBloco(x - 1, y + 1))
                setSubBlocoDe(x - 1, y + 1);
            if (contemBloco(x + 1, y - 1))
                setSubBlocoDe(x + 1, y - 1);
            if (contemBloco(x + 1, y + 1))
                setSubBlocoDe(x + 1, y + 1);
            setSubBlocoDe(x, y);
        }
    }

    public void setSubBlocoDe(int x, int y) {
        if ((blocos[x][y] instanceof BlocoSimples.Complexo) && (Data.temBorda(blocos[x][y].getCodigo()))) {
            BlocoSimples.Complexo b = (BlocoSimples.Complexo) blocos[x][y];
            int codBloco = b.getCodigo();
            boolean[] d = new boolean[8];
            if (contemBloco(x, y - 1))
                d[7] = blocos[x][y - 1].getCodigo() == codBloco;
            if (contemBloco(x, y + 1))
                d[3] = blocos[x][y + 1].getCodigo() == codBloco;
            if (contemBloco(x - 1, y))
                d[5] = blocos[x - 1][y].getCodigo() == codBloco;
            if (contemBloco(x + 1, y))
                d[1] = blocos[x + 1][y].getCodigo() == codBloco;
            if (contemBloco(x - 1, y - 1))
                d[6] = blocos[x - 1][y - 1].getCodigo() == codBloco;
            if (contemBloco(x + 1, y - 1))
                d[0] = blocos[x + 1][y - 1].getCodigo() == codBloco;
            if (contemBloco(x + 1, y + 1))
                d[2] = blocos[x + 1][y + 1].getCodigo() == codBloco;
            if (contemBloco(x - 1, y + 1))
                d[4] = blocos[x - 1][y + 1].getCodigo() == codBloco;
            
            int v = MapConstraints.toInteger(d);
            b.setSubCodigo(MapConstraints.TILE_BORDERS[v][0], MapConstraints.TILE_BORDERS[v][1]);
        }
    }
    
    public void recalcularDelta() {
        recalcularDelta(BLOCK_SIZE, SCREEN_WIDTH, GAME_HEIGHT);
    }

    public void recalcularDelta(int s, int w, int h) {
        if (referencia != -1)
            recalcularDelta(referencia, s, w, h);
    }
    
    public void recalcularDelta(int objetoCentro) {
        recalcularDelta(objetoCentro, BLOCK_SIZE, SCREEN_WIDTH, GAME_HEIGHT);
    }
    
    public BlocoSimples getBlocoOver() {
        return blocos[getReferencia().getPixelX()/BLOCK_SIZE][getReferencia().getPixelY()/BLOCK_SIZE];
    }
    
    public Corpo getObjectOver() {
        for (int i = 0; i < objetos.size(); i++)
            if (i != referencia && colide(objetos.get(i), objetos.get(referencia)))
                return objetos.get(i);
        return null;
    }
    
    private boolean colide(Corpo a, Corpo b) {
        return !(a.getPixelX() + a.getPixelWidth() < b.getPixelX() || a.getPixelY() + a.getPixelHeight() < b.getPixelY() || a.getPixelX() > b.getPixelX() + b.getPixelWidth() || a.getPixelY() > b.getPixelY() + b.getPixelHeight());
    }    

    public void recalcularDelta(int objetoCentro, int s, int w, int h) {
        Corpo nave = objetos.get(objetoCentro);
        deltaS = new Point(s*nave.getPixelX()/BLOCK_SIZE - w/2, s*nave.getPixelY()/BLOCK_SIZE - h/2);
        
        if (deltaS.x < 0)
            deltaS.x = 0;
        else if (deltaS.x > blocos.length*s - w)
            deltaS.x = blocos.length*s - w;
        
        if (deltaS.y < 0)
            deltaS.y = 0;
        else if (deltaS.y > blocos[0].length*s - h)
            deltaS.y = blocos[0].length*s - h;
    }
    
    /**
     * Draws this area, and returns the known area using the previously known
     * area and what becomes known given this draw
     * @param a The current known area to this player
     * @return The new known area to this player
     */
    public Area desenhar(Area a) {
        if (a == null)
            a = new Area(this);
        recalcularDelta();
        
        int mbw = (int) Math.floor(largura / (2*BLOCK_SIZE)) + 1, //quantos blocos cabem em meia horizontal
            mbh = (int) Math.floor(altura / (2*BLOCK_SIZE)) + 1; //quantos blocos cabem em meia vertical
        
        int xCentro = (int) Math.floor(objetos.get(referencia).getPixelX() / BLOCK_SIZE); //em que bloco, aprox., a nave se encontra
        int yCentro = (int) Math.floor(objetos.get(referencia).getPixelY() / BLOCK_SIZE);
        
        int biX = xCentro - mbw, biY = yCentro - mbh,
            bfX = xCentro + mbw, bfY = yCentro + mbh;
        
        if (deltaS.x == 0) {
            biX = 0;
            bfX = mbw*2 + 1;
        } else if (deltaS.x == blocos.length*BLOCK_SIZE - SCREEN_WIDTH) {
            bfX = blocos.length - 1;
            biX = bfX - mbw*2 + 1;
        }
        
        if (deltaS.y == 0) {
            biY = 0;
            bfY = mbh*2 + 1;
        } else if (deltaS.y == blocos[0].length*BLOCK_SIZE - GAME_HEIGHT) {
            bfY = blocos[0].length - 1;
            biY = bfY - mbh*2 + 1;
        }
        
        //First draw the blocks
        BlockLoader.bindBlocks();
        for (int i = biX; i <= bfX; i++)
            for (int j = biY; j <= bfY; j++) {
                BlocoSimples b;
                if (i >= 0 && i < blocos.length && j >= 0 && j < blocos[i].length) {
                    b = blocos[i][j];
                    a.blocos[i][j] = blocos[i][j]; //the player is seing this block; update the knwon area
                } else
                    b = null;
                BlockLoader.desenharBloco(b, (int) (i * BLOCK_SIZE - deltaS.x), (int) (j * BLOCK_SIZE - deltaS.y), BLOCK_SIZE);
            }
        
        //And then the circuits - this is so that we don't need to switch the textures so often
        BlockLoader.bindWires();
        for (int i = biX; i <= bfX; i++)
            for (int j = biY; j <= bfY; j++) {
                BlocoSimples b;
                if (i >= 0 && i < blocos.length && j >= 0 && j < blocos[i].length) {
                    b = blocos[i][j];
                } else
                    b = null;
                BlockLoader.desenharFios(b, (int) (i * BLOCK_SIZE - deltaS.x), (int) (j * BLOCK_SIZE - deltaS.y), BLOCK_SIZE);
            }
        
        //delete previous objects that the player 'remebered', that will now be updated
        for (int i = 0; a.objetos != null && i < a.objetos.size(); i++)
            if (inScreen(a.objetos.get(i))) {
                a.objetos.remove(i);
                i--;
            }
        
        ObjectLoader.bindObjects();
        for (int i = 0; i < objetos.size(); i++) {
            Corpo obj = objetos.get(i);
            
            int ox = (int) (obj.getPixelX() - deltaS.x), oy = (int) (obj.getPixelY() - deltaS.y);
            if (inScreen(ox, oy, obj.getPixelWidth(), obj.getPixelHeight())) { //objeto é visível
                if (obj == selecao)
                    obj.drawSelector(ox, oy);
                obj.draw((int) deltaS.x, (int) deltaS.y);
                obj.updateCache(); //sets the x and y variables to the current body's position
                a.objetos.add(new Kryo().copy(obj)); //creates an copy; it does NOT save the body variable, since it is transient
                if (i == referencia) //assumos yourself is always visible (todo may change?)
                    a.setReferencia();
            } else if (obj instanceof Nave && ((Nave) objetos.get(referencia)).inRange(obj)) //not in screen, but in range
                a.objetos.add(obj);
        }
        
        //finally draw all explosions
        for (Explosao e : explosoes)
            e.draw(deltaS.x, deltaS.y);
        return a;
    }
    
    private boolean inScreen(Corpo m) {
        return inScreen((int) (m.getPixelX() - deltaS.x), (int) (m.getPixelY() - deltaS.y), m.getPixelWidth(), m.getPixelHeight());
    }
    
    private boolean inScreen(int ox, int oy, int w, int h) {
        return ox >= -w && ox <= largura + w && oy >= -h && oy <= altura + h;
    }

    /**
     * Desenha essa área como mapa, ou seja, permite muito mais flexibilidade de opções.
     * Esse método não envolve Known Areas, já que ele pode ser chamado das próprias.
     * @param x X inicial da tela
     * @param y Y inicila da tela
     * @param w Largura do mapa
     * @param h Altura do mapa
     * @param s Tamanho do bloco
     * @param mini Se as texturas mini deverão ser usadas ou não
     */
    public void desenharMapa(int x, int y, int w, int h, int s, boolean mini) {
        recalcularDelta(s, w, h);
        
        int mbw = (int) Math.floor(w / (2*s)) + 1, //quantos blocos cabem em meia horizontal
            mbh = (int) Math.floor(h / (2*s)) + 1; //quantos blocos cabem em meia vertical
        
        int xCentro = (int) Math.floor(objetos.get(referencia).getPixelX() / BLOCK_SIZE); //em que bloco, aprox., a nave se encontra
        int yCentro = (int) Math.floor(objetos.get(referencia).getPixelY() / BLOCK_SIZE);
        
        int biX = xCentro - mbw, biY = yCentro - mbh,
            bfX = xCentro + mbw, bfY = yCentro + mbh;
        
        if (deltaS.x == 0) {
            biX = 0;
            bfX = mbw*2 - 1;
        } else if (deltaS.x == blocos.length*s - w) {
            bfX = blocos.length - 1;
            biX = bfX - mbw*2 + 1;
        }
        
        if (deltaS.y == 0) {
            biY = 0;
            bfY = mbh*2 - 1;
        } else if (deltaS.y == blocos[0].length*s - h) {
            bfY = blocos[0].length - 1;
            biY = bfY - mbh*2 + 1;
        }
        
        BlockLoader.bindBlocks();        
        for (int i = biX; i <= bfX; i++)
            for (int j = biY; j <= bfY; j++) {
                BlocoSimples b;
                if (i >= 0 && i < blocos.length && j >= 0 && j < blocos[i].length)
                    b = blocos[i][j]; //será null caso esteja 'invisível'
                else
                    b = null;
                
                if (mini && false) //todo remove disable mini-textures
                    BlockLoader.desenharMini(b, x + (int) (i * s - deltaS.x), y + (int) (j * s - deltaS.y), s);
                else
                    BlockLoader.desenharBloco(b, x + (int) (i * s - deltaS.x), y + (int) (j * s - deltaS.y), s);
            }
        
        if (!mini) {
            ObjectLoader.bindObjects();
            for (int i = 0; i < objetos.size(); i++) {
                Corpo obj = objetos.get(i);
                int dx = x + (int) (s*obj.getPixelX()/BLOCK_SIZE - deltaS.x), dy = y + (int) (s*obj.getPixelY()/BLOCK_SIZE - deltaS.y);
                int dw = s*obj.getPixelWidth()/BLOCK_SIZE, dh = s*obj.getPixelHeight()/BLOCK_SIZE;
                if (dx >= x - dw && dy >= y - dh && dx <= x + w + dw && dy <= y + h + dh) { //cabe na tela
                    if (obj instanceof SeletorBloco)
                        ((SeletorBloco) obj).draw(dx, dy, s);
                    else if (obj instanceof Nave) {
                        //desenhar representação de um bloco da nave, dependendo da facção
                        Color cor = Faccao.DEFAULT_COLOR;
                        if (((Nave) obj).getEntidade().getFaccao() != null)
                            cor = ((Nave) obj).getEntidade().getFaccao().getCorOficial();
                        ImageInfo.drawColorSquare(dx, dy, dw, dh, cor);
                    }
                }
            }
        }
    }
    
    public boolean selectNearest() {
        if (selectables == null)
            selectables = new ArrayList<>();
        if (objetos.size() == 1) //there is only you in the map
            return false;
        double minDist = 0;
        int sel = -1;
        selectables.clear();
        
        boolean stillInRange = false;
        for (int i = 0; i < objetos.size(); i++) {
            if (i != referencia && objetos.get(i).isSelectable()) {
                double ndist = objetos.get(referencia).dist(objetos.get(i));
                if (ndist <= EntidadeNave.DEFAULT_PERCEPTION_DISTANCE) {
                    selectables.add(objetos.get(i));
                    if (objetos.get(i) == selecao)
                        stillInRange = true;
                    if (sel == -1 || ndist < minDist) {
                        minDist = ndist;
                        sel = i;
                    }
                }
            }
        }
        if (locked && stillInRange) //do not change selection
            return true;
        locked = false;
        if (sel == -1 || minDist > EntidadeNave.DEFAULT_SELECT_DISTANCE) {
            selecao = null; //select nothing
            return false;
        }
        selecao = objetos.get(sel);
        return true;
    }
    
    public ArrayList<Corpo> getSelectables() {
        return selectables;
    }

    public Corpo getSelecao() {
        return selecao;
    }

    public void setSelecao(Corpo selecao) {
        this.selecao = selecao;
    }
    
    public class Passagem implements Serializable {

        private static final long serialVersionUID = -2583419468737525213L;
        private Point.Inteiro local;
        private Area destino;
        private float raio;

        public Passagem(Point.Inteiro local, Area destino, float raio) {
            super();
            this.local = local;
            this.destino = destino;
            this.raio = raio;
        }

        public Point.Inteiro getLocal() {
            return local;
        }

        public void setLocal(Point.Inteiro local) {
            this.local = local;
        }

        public Area getDestino() {
            return destino;
        }

        public void setDestino(Area destino) {
            this.destino = destino;
        }

        public float getRaio() {
            return raio;
        }

        public void setRaio(float raio) {
            this.raio = raio;
        }
    }

    public class Cidade implements Serializable {

        private static final long serialVersionUID = -437750036322783427L;
        private String nome;
        private Point.Inteiro p1, p2; //Limites
        private float influencia, dinheiroAtual;
        private Construcao[] predios;
        private Construcao governo;
        private Habitante[] habitantes;

        public Cidade(String nome, Point.Inteiro p1, Point.Inteiro p2, float influencia,
                float dinheiroAtual, Construcao[] predios, Construcao governo,
                Habitante[] habitantes) {
            super();
            this.nome = nome;
            this.p1 = p1;
            this.p2 = p2;
            this.influencia = influencia;
            this.dinheiroAtual = dinheiroAtual;
            this.predios = predios;
            this.governo = governo;
            this.habitantes = habitantes;
        }

        public String getNome() {
            return nome;
        }

        public void setNome(String nome) {
            this.nome = nome;
        }

        public Point.Inteiro getP1() {
            return p1;
        }

        public void setP1(Point.Inteiro p1) {
            this.p1 = p1;
        }

        public Point.Inteiro getP2() {
            return p2;
        }

        public void setP2(Point.Inteiro p2) {
            this.p2 = p2;
        }

        public float getInfluencia() {
            return influencia;
        }

        public void setInfluencia(float influencia) {
            this.influencia = influencia;
        }

        public float getDinheiroAtual() {
            return dinheiroAtual;
        }

        public void setDinheiroAtual(float dinheiroAtual) {
            this.dinheiroAtual = dinheiroAtual;
        }

        public Construcao[] getPredios() {
            return predios;
        }

        public void setPredios(Construcao[] predios) {
            this.predios = predios;
        }

        public Construcao getGoverno() {
            return governo;
        }

        public void setGoverno(Construcao governo) {
            this.governo = governo;
        }

        public Habitante[] getHabitantes() {
            return habitantes;
        }

        public void setHabitantes(Habitante[] habitantes) {
            this.habitantes = habitantes;
        }

        public abstract class Habitante {

            private int relacao;

            public Habitante(int relacao) {
                this.relacao = relacao;
            }

            public int getRelacao() {
                return relacao;
            }

            public void setRelacao(int relacao) {
                this.relacao = relacao;
            }
        }

        public class HabitanteNave extends Habitante {

            private EntidadeNave nave;
            private float importanciaParcial;

            public HabitanteNave(int relacao, EntidadeNave nave, float importanciaParcial) {
                super(relacao);
                this.nave = nave;
                this.importanciaParcial = importanciaParcial;
            }

            public EntidadeNave getNave() {
                return nave;
            }

            public void setNave(EntidadeNave nave) {
                this.nave = nave;
            }

            public float getImportanciaParcial() {
                return importanciaParcial;
            }

            public void setImportanciaParcial(float importanciaParcial) {
                this.importanciaParcial = importanciaParcial;
            }
        }

        public class HabitanteCriatura extends Habitante {

            private int tipoCriatura;

            public HabitanteCriatura(int relacao, int tipoCriatura) {
                super(relacao);
                this.tipoCriatura = tipoCriatura;
            }

            public int getTipoCriatura() {
                return tipoCriatura;
            }

            public void setTipoCriatura(int tipoCriatura) {
                this.tipoCriatura = tipoCriatura;
            }
        }
    }
}
